今天我們來添加一些套件來管理狀態,並且調整我們的頁面。
現在我們安裝一些套件,jotai zod react-native-gesture-handler react-native-date-picker
!!!!這裡記得安裝完要rebuild,也就是跑yarn android或ios來重新構建(由於react-native-date-picker)。
並且套件如下:
接下來創建我們的狀態管理
創建atom
// src\stores\atoms.ts
import { atom } from 'jotai';
import { Task } from '../types';
export const tasksAtom = atom<Task[]>([]);
在App添加View。
import React, { useState } from 'react';
import { View, Text, TouchableOpacity, StyleSheet, StatusBar, useColorScheme } from 'react-native';
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
import { Colors } from 'react-native/Libraries/NewAppScreen';
import HomePage from './src/pages/home.page';
import AddTaskPage from './src/pages/add-task.page';
import StatisticsPage from './src/pages/statistics-page';
import SettingsPage from './src/pages/settings.page';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
// ...
function App(): React.JSX.Element {
return (
<SafeAreaProvider>
<MainContent />
</SafeAreaProvider>
);
}
// ...
export default App;
接下來調整一下我們的首頁,滑動刪除功能。並使用 Jotai 來管理任務狀態
// src\pages\home.page.tsx
import React, { useCallback } from 'react';
import { View, Text, TextInput, FlatList, StyleSheet, TouchableOpacity } from 'react-native';
import { Colors } from 'react-native/Libraries/NewAppScreen';
import { Task, Tags } from '../types';
import { Swipeable } from 'react-native-gesture-handler';
import { useAtom } from 'jotai';
import { tasksAtom } from '../stores/atoms';
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 10,
},
title: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 10,
},
// ...
tag: {
backgroundColor: '#E0E0E0',
borderRadius: 10,
padding: 5,
marginRight: 5,
marginBottom: 5,
},
tagText: {
fontSize: 12,
},
deleteButton: {
backgroundColor: 'red',
justifyContent: 'center',
alignItems: 'center',
width: 80,
height: '100%',
},
deleteButtonText: {
color: 'white',
fontWeight: 'bold',
},
});
type HomePageProps = {
isDarkMode?: boolean;
};
const generateRandomTasks = (count: number): Task[] => {
const generateRandomDate = (start: Date, end: Date) => {
return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())).toISOString().split('T')[0];
};
const possibleTags: Tags[] = [
{ title: '工作' },
{ title: '個人' },
{ title: '學習' },
{ title: '娛樂' },
];
const tasks: Task[] = [];
for (let i = 0; i < count; i++) {
const startDate = generateRandomDate(new Date(), new Date(Date.now() + 30 * 24 * 60 * 60 * 1000));
const endDate = generateRandomDate(new Date(startDate), new Date(Date.now() + 60 * 24 * 60 * 60 * 1000));
tasks.push({
id: `task-${i + 1}`,
title: `任務 ${i + 1}`,
startDate,
endDate,
description: `隨機生成的第 ${i + 1} 筆內容。`,
isDone: Math.random() < 0.3,
tags: possibleTags.filter(() => Math.random() < 0.3),
subTasks: [],
});
}
return tasks;
};
const HomePage: React.FC<HomePageProps> = ({ isDarkMode = false }) => {
const [tasks, setTasks] = useAtom(tasksAtom);
const toggleTaskStatus = useCallback((id: string) => {
setTasks(prevTasks =>
prevTasks.map(task =>
task.id === id ? { ...task, isDone: !task.isDone } : task
)
);
}, [setTasks]);
const deleteTask = useCallback((id: string) => {
setTasks(prevTasks => prevTasks.filter(task => task.id !== id));
}, [setTasks]);
const renderRightActions = useCallback((id: string) => {
return (
<TouchableOpacity
style={styles.deleteButton}
onPress={() => deleteTask(id)}
>
<Text style={styles.deleteButtonText}>Delete</Text>
</TouchableOpacity>
);
}, [deleteTask]);
const renderTask = useCallback(({ item }: { item: Task }) => (
<Swipeable renderRightActions={() => renderRightActions(item.id)}>
<View
style={[
styles.taskItem,
{ backgroundColor: item.isDone ? '#E8F5E9' : 'transparent' }
]}
>
<TouchableOpacity onPress={() => toggleTaskStatus(item.id)}>
<View style={styles.checkbox}>
{item.isDone && <View style={styles.checkboxInner} />}
</View>
</TouchableOpacity>
<View style={styles.taskContent}>
<Text style={styles.taskTitle}>{item.title}</Text>
<Text style={styles.taskDates}>
{item.startDate} - {item.endDate}
</Text>
<Text style={styles.taskDescription}>{item.description}</Text>
<View style={styles.taskTags}>
{item.tags.map((tag, index) => (
<View key={index} style={styles.tag}>
<Text style={styles.tagText}>{tag.title}</Text>
</View>
))}
</View>
</View>
</View>
</Swipeable>
), [toggleTaskStatus, renderRightActions]);
return (
<View style={styles.container}>
<Text style={styles.title}>任務列表</Text>
<TextInput
style={styles.searchBar}
placeholder="搜尋欄"
placeholderTextColor={isDarkMode ? Colors.light : Colors.dark}
/>
<FlatList
style={styles.taskList}
data={tasks}
renderItem={renderTask}
keyExtractor={item => item.id}
/>
</View>
);
};
export default HomePage;
現在調整一下add task頁面
// src\pages\add-task.page.tsx
import React, { useState } from 'react';
import { View, Text, TextInput, TouchableOpacity, StyleSheet, ScrollView, Alert } from 'react-native';
import { useSetAtom } from 'jotai';
import { tasksAtom } from '../stores/atoms';
import { Task, Tags } from '../types/index';
import DatePicker from 'react-native-date-picker';
type AddTaskPageProps = {
isDarkMode: boolean;
};
const AddTaskPage: React.FC<AddTaskPageProps> = ({ isDarkMode }) => {
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [startDate, setStartDate] = useState<Date | null>(null);
const [endDate, setEndDate] = useState<Date | null>(null);
const [openStartDate, setOpenStartDate] = useState(false);
const [openEndDate, setOpenEndDate] = useState(false);
const [tags, setTags] = useState<Tags[]>([]);
const [currentTag, setCurrentTag] = useState('');
const setTasks = useSetAtom(tasksAtom);
const validateTask = (task: Task): string[] => {
const errors: string[] = [];
if (!task.title.trim()) {
errors.push('Title is required');
}
if (task.startDate && task.endDate && new Date(task.startDate) > new Date(task.endDate)) {
errors.push('End date should be after start date');
}
return errors;
};
const handleSubmit = () => {
const newTask: Task = {
id: Date.now().toString(),
title,
startDate: startDate ? startDate.toISOString().split('T')[0] : null,
endDate: endDate ? endDate.toISOString().split('T')[0] : null,
description,
isDone: false,
tags,
subTasks: [],
};
const validationErrors = validateTask(newTask);
if (validationErrors.length > 0) {
Alert.alert('Validation Error', validationErrors.join('\n'));
return;
}
setTasks(prevTasks => [...prevTasks, newTask]);
// Reset form fields
setTitle('');
setDescription('');
setStartDate(null);
setEndDate(null);
setTags([]);
Alert.alert('Success', 'Task added successfully');
};
const handleAddTag = () => {
if (currentTag.trim()) {
setTags([...tags, { title: currentTag.trim() }]);
setCurrentTag('');
}
};
const handleRemoveTag = (index: number) => {
setTags(tags.filter((_, i) => i !== index));
};
return (
<ScrollView style={styles.container}>
<Text style={styles.title}>新增任務畫面</Text>
<TextInput
style={styles.input}
placeholder="任務標題"
value={title}
onChangeText={setTitle}
/>
<TextInput
style={[styles.input, styles.multilineInput]}
placeholder="任務描述"
multiline
numberOfLines={3}
value={description}
onChangeText={setDescription}
/>
<TouchableOpacity style={styles.input} onPress={() => setOpenStartDate(true)}>
<Text>{startDate ? startDate.toLocaleDateString() : '選擇開始日期'}</Text>
</TouchableOpacity>
<DatePicker
modal
open={openStartDate}
date={startDate || new Date()}
onConfirm={(date) => {
setOpenStartDate(false);
setStartDate(date);
}}
onCancel={() => {
setOpenStartDate(false);
}}
mode="date"
/>
<TouchableOpacity style={styles.input} onPress={() => setOpenEndDate(true)}>
<Text>{endDate ? endDate.toLocaleDateString() : '選擇結束日期'}</Text>
</TouchableOpacity>
<DatePicker
modal
open={openEndDate}
date={endDate || new Date()}
onConfirm={(date) => {
setOpenEndDate(false);
setEndDate(date);
}}
onCancel={() => {
setOpenEndDate(false);
}}
mode="date"
/>
<View style={styles.tagInputContainer}>
<TextInput
style={styles.tagInput}
placeholder="輸入標籤"
value={currentTag}
onChangeText={setCurrentTag}
onSubmitEditing={handleAddTag}
/>
<TouchableOpacity style={styles.addTagButton} onPress={handleAddTag}>
<Text style={styles.addTagButtonText}>添加</Text>
</TouchableOpacity>
</View>
<View style={styles.tagsContainer}>
{tags.map((tag, index) => (
<TouchableOpacity
key={index}
style={styles.tag}
onPress={() => handleRemoveTag(index)}
>
<Text style={styles.tagText}>{tag.title}</Text>
</TouchableOpacity>
))}
</View>
<TouchableOpacity style={styles.submitButton} onPress={handleSubmit}>
<Text style={styles.submitButtonText}>新增任務</Text>
</TouchableOpacity>
</ScrollView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 10,
},
title: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 10,
},
input: {
height: 40,
borderColor: 'gray',
borderWidth: 1,
borderRadius: 5,
paddingHorizontal: 10,
marginBottom: 10,
justifyContent: 'center',
},
multilineInput: {
height: 80,
},
tagInputContainer: {
flexDirection: 'row',
marginBottom: 10,
},
tagInput: {
flex: 1,
height: 40,
borderColor: 'gray',
borderWidth: 1,
borderRadius: 5,
paddingHorizontal: 10,
marginRight: 10,
},
addTagButton: {
backgroundColor: '#007AFF',
padding: 10,
borderRadius: 5,
justifyContent: 'center',
},
addTagButtonText: {
color: 'white',
fontWeight: 'bold',
},
tagsContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
marginBottom: 10,
},
tag: {
backgroundColor: '#E0E0E0',
borderRadius: 15,
padding: 5,
paddingHorizontal: 10,
margin: 2,
},
tagText: {
fontSize: 12,
},
submitButton: {
backgroundColor: '#007AFF',
padding: 10,
borderRadius: 5,
alignItems: 'center',
marginTop: 10,
},
submitButtonText: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
},
});
export default AddTaskPage;
今天調整了首頁和新增頁面,並使用了jotai作為我們的狀態管理。明天會接續將其他部分完善。